'use strict'

/** @private */
let Fiber = require('fibers');
/** @private */
let um = require('..');
/** @private */
let BaseModel = um.BaseModel;
/** @private */
let ModelStatus = BaseModel.Status;

/**
 * The base class of backend data access object.
 *
 * @public
 */
class Dao {
    /**
     * The constructor of Dao class
     *
     * @public
     * @param {backend/base/Session} [session] - The backend session object.
     */
    constructor(session) {
        this.session = session;
    }

    /**
     * Convert the UniqueModel object to BackendModel object.
     *
     * @public
     *
     * @param {model/base/Model} [uniqueModelObject] - The UniqueModel object to convert.
     *
     * @return {Object} The backend model object corresponding to the UniqueModel object.
     */
    toBackendModel(uniqueModelObject) {
        let fields = Object.create(null);
        let schema = this.uniqueModel._meta.schema;

        if ( !uniqueModelObject._dao || uniqueModelObject._complete ) {
            for ( let fieldName in schema ) {
                let fieldType = schema[fieldName];
                if ( uniqueModelObject[fieldName] != null && 
                  uniqueModelObject[fieldName] != undefined ) {
                    fields[fieldName] = uniqueModelObject[fieldName];

                    if ( fieldType.name === 'UObject' ) {
                        fields[fieldName] = fields[fieldName].id;
                    }
                    else if ( fieldType.name === 'UObjectArray' ) {
                        fields[fieldName] = fields[fieldName].map(element => {
                            return element.id;
                        });
                    }
                }
            }
        }

        if ( 'id' in uniqueModelObject && uniqueModelObject.id ) {
            fields._id = uniqueModelObject.id;
        }

        return fields;
    }

    /**
     * Convert the backend model object object to UniqueModel object.
     *
     * @public
     *
     * @param {Object} [backendModelObject] - The backend model object object to convert.
     * @param {model/base/Model} [uniqueModelObject] - The UniqueModel object to store the converted fields.
     *
     * @return {model/base/Model} The UniqueModel object corresponding to the backend model object.
     */
    toUniqueModel(backendModelObject, uniqueModelObject) {
        let self = this;
        let uniqueModel = this.uniqueModel;
        let schema = uniqueModel._meta.schema;

        if ( !uniqueModelObject ) {
            uniqueModelObject = new uniqueModel();
        }

        for ( let fieldName in schema ) {
            let fieldType = schema[fieldName];

            let fieldValue = backendModelObject[fieldName];
            if ( fieldType.name === 'UObject' && fieldValue ) {
                let subModel = um.model.getModel(fieldType.type);
                let subModelObject = new subModel();
                subModelObject.id = String(fieldValue);
                subModelObject._dao = this.session.getDao(subModel);

                uniqueModelObject._data[fieldName] = subModelObject;
            }
            else if ( fieldType.name === 'UObjectArray' ) {
                let subModel = um.model.getModel(fieldType.type);
                let modelIdArray = fieldValue;

                let subModelObjects = modelIdArray.map(modelId => {
                    let subModelObject = new subModel();
                    subModelObject.id = String(modelId);
                    subModelObject._dao = self.session.getDao(subModel);

                    return subModelObject;
                });

                uniqueModelObject._data[fieldName] = subModelObjects;
            }
            else {
                uniqueModelObject._data[fieldName] = fieldType.fromValue(fieldValue);
            }
        }
        uniqueModelObject.id = backendModelObject._id;

        return uniqueModelObject;
    }

    /**
     * Convert the Array of backend model objects to Array of UniqueModel objects.
     *
     * @public
     *
     * @param {Array} [backendModelObjectArray] - The Array of backend model objects object to convert.
     *
     * @return {Array} The Array of UniqueModels  object corresponding to the Array of backend model objects.
     */
    toUniqueModelArray(backendModelObjectArray) {
        let self = this;

        return backendModelObjectArray.map(backendModelObject => {
                return self.toUniqueModel(backendModelObject);
            }
        );
    }

    /**
     * Execute the asynchronous operation of backend model object.
     * It use Fibers to convert asynchronous to synchronous operation.
     * If the asynchronous pass a error, then this function will throw exception.
     *
     * @public
     *
     * @param {String} [operation] - The operation to execute.
     *
     * @return {*} The operation result.
     */
    execute(operation) {
        let fiber = Fiber.current;
        let error = null;
        let result = null;
        let args = Array.from(arguments);

        let operationAsync = operation + 'Async';
        let callback = function(err, res) {
            error = err;
            result = res;
    
            fiber.run();
        }
        args = args.slice(1);
        args.push(callback);

        this[operationAsync].apply(this, args);
        Fiber.yield();
    
        if ( error ) {
            throw error;
        }
    
        return result;
    }

    /**
     * Create the UniqueModel object and persist it through backend.
     * It will call the asyncCreate method by using execute method.
     *
     * @public
     *
     * @param {model/base/Model} [uniqueModelObject] - The UniqueModel object to create.
     *
     * @return {model/base/Model} The created UniqueModel object.
     */
    create(uniqueModelObject) {
        let backendModelObject = this.toBackendModel(uniqueModelObject);
        let createdBackendModelObject = this.execute('create', backendModelObject);
        this.toUniqueModel(createdBackendModelObject, uniqueModelObject);

        if ( uniqueModelObject ) {
            uniqueModelObject._dao = this;
            uniqueModelObject._status = ModelStatus.Managed;
            uniqueModelObject._complete = true;
        }

        return uniqueModelObject;
    }

    /**
     * Insert the plain objects and persist it through backend.
     * It will call the asyncInsert method by using execute method.
     *
     * @public
     *
     * @param {model/base/Model} [Array] - The UniqueModel object to create.
     */
    insertMany(plainObjects) {
        this.execute('insertMany', plainObjects);
    }

    /**
     * Refresh the persisted UniqueModel object.
     * It will load the UniqueModel object from backend session according to the id of object.
     * It will call the findOne method to get the data.
     *
     * @public
     *
     * @param {model/base/Model} [uniqueModelObject] - The UniqueModel object to refresh.
     *
     * @return {model/base/Model} The refreshed UniqueModel object.
     */
    refresh(uniqueModelObject) {
        let backendModelObject = this.toBackendModel(uniqueModelObject);
        let condition = {
            [this.getObjectIdKey()]: this.getObjectId(backendModelObject)
        };
        let foundBackendModelObject = this.execute('findOne', condition);

        if ( !foundBackendModelObject ) {
            return null;
        }

        this.toUniqueModel(foundBackendModelObject, uniqueModelObject);
        uniqueModelObject._dao = this;
        uniqueModelObject._status = ModelStatus.Managed;
        uniqueModelObject._complete = true;

        return uniqueModelObject;
    }

    replaceIdInCondition(condition) {
        let objectIdCondition = condition.id;
        if ( objectIdCondition != null || objectIdCondition != undefined ) {
            delete condition.id;
            condition[this.getObjectIdKey()] = objectIdCondition;
        }
    }

    count(condition) {
        this.replaceIdInCondition(condition);

        let count = this.execute('count', condition);

        return count;
    }

    aggregate(condition) {
        this.replaceIdInCondition(condition);

        let result = this.execute('aggregate', condition);

        return result;
    }

    distinct(field, condition) {
        this.replaceIdInCondition(condition);

        let foundBackendModelObjects = this.execute('distinct', field, condition);

        return foundBackendModelObjects;
    }

    /**
     * Find the objects by condition.
     * If the field of object is another object, it will only get the id of object,
     * and the other fields will be loaded when you first access the object.
     *
     * @public
     *
     * @param {Object} [condition] - The query condition
     *
     * @return {Array} The Array of UniqueModel objects that match conditions.
     */
    find(condition) {
        this.replaceIdInCondition(condition);

        let foundBackendModelObjects = this.execute('find', condition);
        let foundUniqueModelObjects = this.toUniqueModelArray(foundBackendModelObjects);
        foundUniqueModelObjects.forEach((uniqueModelObject) => {
            uniqueModelObject._dao = this;
            uniqueModelObject._status = ModelStatus.Managed;
        });

        return foundUniqueModelObjects;
    }

    /**
     * Find one object by condition.
     * If the field of object is another object, it will only get the id of object,
     * and the other fields will be loaded when you first access the object.
     *
     * @public
     *
     * @param {Object} [condition] - The query condition
     *
     * @return {model/base/Model} The UniqueModel object that match conditions.
     */
    findOne(condition) {
        this.replaceIdInCondition(condition);

        let foundBackendModelObject = this.execute('findOne', condition);
        if ( !foundBackendModelObject ) {
            return null;
        }

        let foundUniqueModelObject = this.toUniqueModel(foundBackendModelObject);
        foundUniqueModelObject._dao = this;
        foundUniqueModelObject._status = ModelStatus.Managed;
        foundUniqueModelObject._complete = true;

        return foundUniqueModelObject;
    }

    /**
     * Update the objects match some conditions.
     * The update operation will be performed according to the argument doc.
     *
     * @public
     *
     * @param {Object} [condition] - The query condition
     * @param {Object} [doc] - The update actions
     * @param {Object} [options] - The update options
     * @param {Boolean} [options.multi] - Update multiple objects if it is true, or only one object will be updated.
     *
     * @return {Array} The Array of updated UniqueModel objects that match conditions.
     */
    update(condition, doc, options) {
        if ( condition instanceof um.BaseModel ) {
            doc = this.toBackendModel(condition);
            condition = {
                [this.getObjectIdKey()]: this.getObjectId(doc)
            };

            // We don't update the id.
            delete doc[this.getObjectIdKey()];

            this.execute('updateOne', condition, doc, options);

            return;
        }
        else {
            this.replaceIdInCondition(condition);
        }

        return this.execute('update', condition, doc, options);
    }

    /**
     * Update only one object match some conditions.
     * The update operation will be performed according to the argument doc.
     *
     * @public
     *
     * @param {Object} [condition] - The query condition
     * @param {Object} [doc] - The update actions
     * @param {Object} [options] - The update options
     * @param {Boolean} [options.new] - updateOne will return updated object when this argument is true.
     *                                  Or it will return the object before update.
     *
     * @return {model/base/Model} The updated UniqueModel object that match conditions.
     */
    updateOne(condition, doc, options) {
        if ( condition instanceof um.BaseModel ) {
            doc = this.toBackendModel(condition);
            condition = {
                [this.getObjectIdKey()]: this.getObjectId(doc)
            };

            // We don't update the id.
            delete doc[this.getObjectIdKey()];

            this.execute('updateOne', condition, doc, options);

            return;
        }
        else {
            this.replaceIdInCondition(condition);
        }

        let foundBackendModelObject = this.execute('updateOne', condition, doc, options);
        let foundUniqueModelObject = this.toUniqueModel(foundBackendModelObject);
        foundUniqueModelObject._dao = this;
        foundUniqueModelObject._status = ModelStatus.Managed;
        foundUniqueModelObject._complete = true;

        return foundUniqueModelObject;
    }

    /**
     * Remove some objects.
     * If the argument is an UniqueModel object, the method will remove this object from persistent session only.
     * If the argument is an condition object, the method will remove all objects match conditions.
     *
     * @public
     *
     * @param {Object} [condition] - The UniqueModel object or condition object.
     */
    remove(condition) {
        if ( condition instanceof um.BaseModel ) {
            let uniqueModelObject = condition;
            let backendModelObject = this.toBackendModel(uniqueModelObject);
            condition = {
                [this.getObjectIdKey()]: this.getObjectId(backendModelObject)
            };

            uniqueModelObject.status = ModelStatus.Removed;
        }
        else {
            this.replaceIdInCondition(condition);
        }

        return this.execute('remove', condition);
    }

    // interface
    createAsync(backendModel, callback) {
        callback(null, null);
    }

    // interface
    insertManyAsync(plainObjects, callback) {
        callback(null, null);
    }

    // interface
    getObjectIdKey() {
        return null;
    }

    getObjectId(backendModelObject) {
        return backendModelObject[this.getObjectIdKey()];
    }

    // override
    findAsync(condition, callback) {
        callback(null, null);
    }

    // override
    countAsync(condition, callback) {
        callback(null, null);
    }

    // override
    aggregateAsync(condition, callback) {
        callback(null, null);
    }

    // override
    distinctAsync(field, condition, callback) {
        callback(null, null);
    }

    // override
    findOneAsync(condition, callback) {
        callback(null, null);
    }

    // override
    updateAsync(condition, doc, callback) {
        callback(null, null);
    }

    // override
    updateOneAsync(condition, doc, callback) {
        callback(null, null);
    }

    // override
    removeAsync(condition, callback) {
        callback(null, null);
    }
}

class Session {
    constructor(options) {
        this.daos = Object.create(null);
    }

    getDao(model) {
        if ( !(model._meta.name in this.daos) ) {
            this.daos[model._meta.name] = this.createDao(model);
        }

        return this.daos[model._meta.name];
    }

    // interface
    createDao(model) {
        return null;
    }
}

module.exports = {
        Dao,
        Session
};
